﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Db4objects.Db4o.Linq;
using Db4objects.Db4o.Query;
using Db4objects.Db4o;
using ExtremeSwank.OpenId;
using ExtremeSwank.OpenId.Persistence;

namespace ExtremeSwank.OpenId.Persistence
{
    /// <summary>
    /// Persists OpenID Stateful association data using a db4o database.
    /// </summary>
    /// <remarks>
    /// Implemented using Linq queries, so requires db4o 7.2, and .NET 3.5 or higher.
    /// </remarks>
    public class Db4oAssociationManager : IAssociationPersistence, IDisposable
    {
        IObjectContainer db;
        bool directFile;

        /// <summary>
        /// Create a new instance of Db4oAssociationManager using an already-open IObjectContainer.
        /// </summary>
        /// <param name="container">The IObjectContainer to use.</param>
        public Db4oAssociationManager(IObjectContainer container)
        {
            db = container;
        }

        /// <summary>
        /// Create a new instance of Db4oAssociationManager.
        /// </summary>
        /// <param name="databasefile">The database file to open directly.</param>
        public Db4oAssociationManager(string databasefile)
        {
            db = Db4oFactory.OpenFile(databasefile);
            directFile = true;
        }

        #region IAssociationPersistence Members

        /// <summary>
        /// Removes an association from the database.
        /// </summary>
        /// <param name="assoc">Association to remove.</param>
        public void Remove(Association assoc)
        {
            var result = from a in db.Query<Association>()
                         where a.Handle == assoc.Handle
                         select a;
            foreach (Association a in result)
            {
                db.Delete(a);
            }
            db.Commit();
        }

        /// <summary>
        /// Add an assocation to the database.
        /// </summary>
        /// <param name="association">Association to add.</param>
        public void Add(Association association)
        {
            db.Store(association);
            db.Commit();
        }

        /// <summary>
        /// Locate an association by its handle.
        /// </summary>
        /// <param name="handle">The association's handle.</param>
        /// <returns>A populated Association object, or null if not found.</returns>
        public Association FindByHandle(string handle)
        {
            var result = (from a in db.Query<Association>()
                         where a.Handle == handle
                         select a).ToList();
            if (result.Count > 0) 
            {
                return result[0];
            }
            return null;
        }

        /// <summary>
        /// Locate an association by its OpenID Server URL.
        /// </summary>
        /// <param name="server">The OpenID Server URL.</param>
        /// <returns>A populated Association object, or null if not found.</returns>
        public Association FindByServer(string server)
        {
            var result = (from a in db.Query<Association>()
                         where a.Server == server
                         select a).ToList();
            if (result.Count > 0)
            {
                return result[0];
            }
            return null;
        }

        /// <summary>
        /// Remove all expired associations from the database.
        /// </summary>
        public void Cleanup()
        {
            DateTime now = DateTime.UtcNow;

            AssociationLastCleanup alc = new AssociationLastCleanup(now);
            var cleanup = (from c in db.Query<AssociationLastCleanup>() select c).ToList();
            if (cleanup.Count > 0)
            {
                alc = cleanup[0];
            }

            if (alc.LastCleanup <= now.AddMinutes(-10))
            {
                var result = (from a in db.Query<Association>()
                             where a.Expiration <= now
                             select a).ToList();
                foreach (Association a in result)
                {
                    db.Delete(a);
                }
            }

            alc.LastCleanup = now;
            db.Store(alc);
            db.Commit();
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// If the database file was opened directly, clean up and close the file.
        /// </summary>
        public void Dispose()
        {
            if (directFile)
            {
                if (db != null)
                {
                    db.Close();
                    db.Dispose();
                    db = null;
                }
            }
        }

        #endregion
    }

    internal class AssociationLastCleanup
    {
        public AssociationLastCleanup(DateTime lastcleanup)
        {
            LastCleanup = lastcleanup;
        }
        public DateTime LastCleanup { get; set; }
    }
}
